
/**
 * 继承是面向对象的三大特征之一，也是实现软件复用的重要手段。Java的继承具有单继承的特点，每个子类只有一个直接父类。
 * <p>
 * Java的继承通过extends关键字来实现，实现继承的类被称为子类，被继承的类被称为父类，有的也称其为基类、超类。
 * 父类和子类的关系，是一种一般和特殊的关系。
 * <p>
 * Java里子类继承父类的语法格式如下：
 * <pre>
 *     修饰符 class SubClass extends SuperClass
 *     {
 *         // 类定义部分
 *     }
 * </pre>
 * <p>
 * 注意：子类只能从被扩展的父类获得成员变量、方法和内部类（包括内部接口、枚举），不能获得构造器和初始化块。
 */
// 下面是Fruit类的代码。
class Fruit {
    public double weight;

    public void info() {
        System.out.println("我是一个水果！重" + weight + "g!");
    }
}

// 接下来再定义该Fruit类的子类Apple，程序如下。
class Apple extends Fruit {
    public static void main() {
        // 创建Apple对象
        Apple apple = new Apple();
        // Apple对象本身没有weight成员变量
        // 因为Apple的父类有weight成员变量，也可以访问Apple对象的weight成员变量
        apple.weight = 56;
        // 调用Apple对象的info方法
        apple.info();
    }
}

/**
 * Java语言摒弃了C++中难以理解的多继承特征，即每个类最多只有一个直接父类。
 * 很多书在介绍Java的单继承时，可能会说Java类只能有一个父类，严格来讲，这种说法是错误的，
 * 应该换成如下说法：Java类只能有一个直接父类，实际上，Java类可以有无限多个间接父类。
 * <p>
 * 如果定义一个Java类时并未显式指定这个类的直接父类，则这个类默认扩展java.lang.Object类。
 * 因此，java.lang.Object类是所有类的父类，要么是其直接父类，要么是其间接父类。
 * 因此所有的Java对象都可调用java.lang.Object类所定义的实例方法。
 */

/**
 * 重写父类的方法
 */
// 下面程序先定义了一个Bird类。
class Bird {
    public void fly() {
        System.out.println("我在天空里自由自在地飞翔...");
    }
}

// 下面再定义一个Ostrich类，这个类扩展了Bird类，重写了Bird类的fly()方法。
class Ostrich extends Bird {
    public void fly() {
        System.out.println("我只能在地上奔跑...");
    }

    public static void main() {
        // 创建Ostrich对象
        Ostrich os = new Ostrich();
        // 执行Ostrich对象的fly方法，输出"我只能在地上奔跑..."
        os.fly();
    }
}

/**
 * 执行上面程序，将看到执行os.fly()时执行的不再是Bird类的fly()方法，而是执行Ostrich类的fly()方法。
 * 这种子类包含与父类同名方法的现象被称为方法重写（Override），也被称为方法覆盖。可以说子类重写了父类的方法，也可以说子类覆盖了父类的方法。
 * <p>
 * 方法的重写要遵循“两同两小一大”规则，
 * > “两同”即方法名相同、形参列表相同；
 * > “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等，子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等；
 * > “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
 * 尤其需要指出的是，覆盖方法和被覆盖方法要么都是类方法，要么都是实例方法，不能一个是类方法，一个是实例方法。
 */
//  下面编译错误
class BaseClass {
    public static void test() {
    }
}

class SubClass extends BaseClass {
    // 编译错误
//    public static test() {}
}

/**
 * 当子类覆盖了父类方法后，子类的对象将无法访问父类中被覆盖的方法，但可以在子类方法中调用父类中被覆盖的方法。
 * 如果需要在子类方法中调用父类中被覆盖的方法，则可以使用super（被覆盖的是实例方法）或者父类类名（被覆盖的是类方法）作为调用者来调用父类中被覆盖的方法。
 * <p>
 * 如果父类方法具有private访问权限，则该方法对其子类是隐藏的，因此其子类无法访问该方法，也就是无法重写该方法。
 * 如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法，依然不是重写，只是在子类中重新定义了一个新方法。
 */
class BaseClass1 {
    // test()方法是private访问权限，子类不可访问该方法
    private void test() {
    }
}

class SubClass1 extends BaseClass1 {
    // 此处并不是方法重写，所以可以增加static关键字
    public static void test() {
    }
}

/**
 * 载和重写的区别？
 * 方法重载和方法重写在英语中分别是overload和override，
 * 其实把重载和重写放在一起比较本身没有太大的意义，因为重载主要发生在同一个类的多个同名方法之间，而重写发生在子类和父类的同名方法之间。
 * 它们之间的联系很少，除二者都是发生在方法之间，并要求方法名相同之外，没有太大的相似之处。
 * 当然，父类方法和子类方法之间也可能发生重载，因为子类会获得父类方法，
 * 如果子类定义了一个与父类方法有相同的方法名，但参数列表不同的方法，就会形成父类方法和子类方法的重载。
 */
// 子类方法与父类方法发送重载
class BaseClass2 {
    public void test() {
        System.out.println("父类无参test方法");
    }
}

class SubClass2 extends BaseClass2 {
    public void test(int a) {
        System.out.println("子类有参test方法");
    }

    public static void main() {
        SubClass2 sc2 = new SubClass2();
        sc2.test();         // 父类无参test方法
        sc2.test(10);   // 子类有参test方法
    }
}

/**
 * super限定
 * 如果需要在子类方法中调用父类被覆盖的实例方法，则可使用super限定来调用父类被覆盖的实例方法。
 * <p>
 * super是Java提供的一个关键字，super用于限定该对象调用它从父类继承得到的实例变量或方法。
 * 正如this不能出现在static修饰的方法中一样，super也不能出现在static修饰的方法中。
 */
class BaseClass3 {
    public void test() {
        System.out.println("父类无参test方法");
    }
}

class SubClass3 extends BaseClass3 {
    public void test() {
        System.out.println("子类无参test方法");
    }

    public void callOverridedMethod() {
        // 在子类方法中通过super显式调用父类被覆盖的示例方法
        super.test();
    }

    public static void main() {
        SubClass3 sc3 = new SubClass3();
        sc3.test();         // 子类无参test方法
        sc3.callOverridedMethod();
    }
}

/**
 * 如果子类定义了和父类同名的实例变量，则会发生子类实例变量隐藏父类实例变量的情形。
 * 在正常情况下，子类里定义的方法直接访问该实例变量默认会访问到子类中定义的实例变量，无法访问到父类中被隐藏的实例变量。
 * 在子类定义的实例方法中可以通过super来访问父类中被隐藏的实例变量，
 */
class BaseClass4 {
    public int a = 5;
}

class SubClass4 extends BaseClass4 {
    public int a = 7;

    public void accessOwner() {
        // 当前类的变量a
        System.out.println(a);
    }

    public void accessBase() {
        // 通过super来限定访问从父类继承得到的a实例变量
        System.out.println(super.a);
    }

    public static void main() {
        SubClass4 sc4 = new SubClass4();
        sc4.accessOwner();  // 输出7
        sc4.accessBase();   // 输出5
    }
}

/**
 * 如果子类里没有包含和父类同名的成员变量，那么在子类实例方法中访问该成员变量时，则无须显式使用super或父类名作为调用者。
 * 如果在某个方法中访问名为a的成员变量，但没有显式指定调用者，则系统查找a的顺序为：
 * ①查找该方法中是否有名为a的局部变量。
 * ②查找当前类中是否包含名为a的成员变量。
 * ③查找a的直接父类中是否包含名为a的成员变量，依次上溯a的所有父类，直到java.lang.Object类，如果最终不能找到名为a的成员变量，则系统出现编译错误。
 * <p>
 * 如果被覆盖的是类变量，在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量。
 */

/**
 * 因为子类中定义与父类中同名的实例变量并不会完全覆盖父类中定义的实例变量，
 * 它只是简单地隐藏了父类中的实例变量，所以会出现如下特殊的情形。
 */
class Parent {
    public String tag = "学习Java";
}

class Derived extends Parent {
    // 定义一个私有的tag变量来隐藏父类的tag实例变量
    private String tag = "Java类的继承";
}

/**
 * 子类不会获得父类的构造器，但子类构造器里可以调用父类构造器的初始化代码，类似于前面所介绍的一个构造器调用另一个重载的构造器。
 * 在一个构造器中调用另一个重载的构造器使用this调用来完成，在子类构造器中调用父类构造器使用super调用来完成。
 * 看下面程序定义了Base类和Sub类，其中Sub类是Base类的子类，程序在Sub类的构造器中使用super来调用Base构造器的初始化代码。
 */
class Base {
    public double size;
    public String name;

    public Base(double size, String name) {
        this.size = size;
        this.name = name;
    }
}

class Sub extends Base {
    public String color;

    public Sub(double size, String name, String color) {
        // 通过super来调用父类的构造器
        super(size, name);
        this.color = color;
    }

    public static void main() {
        Sub s = new Sub(10.1, "Java", "咖啡色");
        System.out.println(s.size + " " + s.name + " " + s.color);
    }
}

/**
 * 从上面程序中不难看出，使用super调用和使用this调用也很像，区别在于super调用的是其父类的构造器，而this调用的是同一个类中重载的构造器。
 * 因此，使用super调用父类构造器也必须出现在子类构造器执行体的第一行，所以this调用和super调用不会同时出现。
 * <p>
 * 不管是否使用super调用来执行父类构造器的初始化代码，子类构造器总会调用父类构造器一次。
 * 子类构造器调用父类构造器分如下几种情况。
 * ➢ 子类构造器执行体的第一行使用super显式调用父类构造器，系统将根据super调用里传入的实参列表调用父类对应的构造器。
 * ➢ 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器，系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时也会先调用父类构造器。
 * ➢ 子类构造器执行体中既没有super调用，也没有this调用，系统将会在执行子类构造器之前，隐式调用父类无参数的构造器。
 * <p>
 * 不管上面哪种情况，当调用子类构造器来初始化子类对象时，父类构造器总会在子类构造器之前执行；
 * 不仅如此，执行父类构造器时，系统会再次上溯执行其父类构造器……依此类推，创建任何Java对象，最先执行的总是java.lang.Object类的构造器。
 */

// 下面程序定义了三个类，它们之间有严格的继承关系，通过这种继承关系可以看到构造器之间的调用关系。
class Creature {
    public Creature() {
        System.out.println("Creature无参数的构造器");
    }
}

class Animal extends Creature {
    public Animal(String name) {
        System.out.println("Animal带一个参数的构造器，该动物的名字为" + name);
    }

    public Animal(String name, int age) {
        this(name); // 使用this调用同一个重载的构造器
        System.out.println("Animal带两个参数的构造器，其age为" + age);
    }
}

class Wolf extends Animal {
    public Wolf() {
        // 显式调用父类有两个参数的构造器
        super("灰太狼", 3);
        System.out.println("Wolf无参数的构造器");
    }

    public static void main() {
        new Wolf();
    }
}

/**
 * 从上面运行过程来看，创建任何对象总是从该类所在继承树最顶层类的构造器开始执行的，然后依次向下执行，最后才执行本类的构造器。
 * 如果某个父类通过this调用了同类中重载的构造器，就会依次执行此父类的多个构造器。
 */

public class Inheritance {
    public static void HideTest() {
        Derived d = new Derived();
        // 程序不可以访问d的私有变量tag，所以下面语句引起编译错误
//        System.out.println(d.tag);
        // 将d变量显式地向上转型为Parent后，即可访问tag实例变量
        System.out.println(((Parent) d).tag);
    }

    public static void main(String[] args) {
        Apple.main();
        Ostrich.main();
        SubClass2.main();
        SubClass3.main();
        SubClass4.main();
        Sub.main();
        Wolf.main();
    }
}
